import json
from datetime import datetime
from typing import Optional, List

from bson import ObjectId
from bson.errors import InvalidId
from pydantic import BaseModel, BaseConfig, validator

from exceptions import InvalidParam
from utils.dateutil import format_date, date_now


class MissingFields(Exception):
    def __init__(self, field):
        self.msg = f"required {field}, but get None"


class OID(str):
    @classmethod
    def __get_validators__(cls):
        yield cls.validate

    @classmethod
    def validate(cls, v):
        try:
            return ObjectId(str(v))
        except InvalidId:
            raise ValueError("Not a valid ObjectId")


def retrieve_attr(obj, attr):
    for name in attr.split("."):
        obj = getattr(obj, name)
    return obj


def is_empty_value(v):
    if isinstance(v, (int, float, bool)):
        return v is None
    return False if v else True


class DictStr(dict):
    @classmethod
    def __get_validators__(cls):
        yield cls.validate

    @classmethod
    def validate(cls, v):
        try:
            return json.dumps(v, ensure_ascii=False)
        except InvalidId:
            raise ValueError("Not a valid dict")


class MongoModel(BaseModel):
    ocode_num: Optional[int]  # object code number (for sorting)
    ocode: Optional[str]      # object code

    class Config(BaseConfig):
        allow_population_by_field_name = True
        json_encoders = {
            datetime: lambda dt: format_date(dt),
            ObjectId: lambda oid: str(oid),
            OID: lambda oid: str(oid),
        }

    @classmethod
    def from_mongo(cls, data: dict):
        """We must convert _id into "id". """
        if not data:
            return data
        id = data.pop('_id', None)
        return cls(**dict(data, id=id))

    def mongo(self, **kwargs):
        exclude_unset = kwargs.pop('exclude_unset', True)
        by_alias = kwargs.pop('by_alias', True)

        parsed = self.dict(
            exclude_unset=exclude_unset,
            by_alias=by_alias,
            **kwargs,
        )

        # Mongo uses `_id` as default key. We should stick to that as well.
        if '_id' not in parsed and 'id' in parsed:
            parsed['_id'] = parsed.pop('id')

        return parsed

    def set_ocode(self, ocode: tuple):
        self.ocode_num = ocode[0]
        self.ocode = ocode[1]

    def fields_check(self, include: List[str] = [], exclude: List[str] = [], include_opt="and"):
        """
        @param include: 必须包含的字段
        @param exclude: 不能包含的字段
        @param include_opt: and - 每个字段必须包含，or - 其中一个包含就行
        """
        both = set(include) & set(exclude)
        if both:
            raise ValueError(f"fields are both exists in include and exclude, fields: {both}")

        no_exists = []
        for field in include:
            if is_empty_value(retrieve_attr(self, field)):
                if include_opt == "and":
                    raise InvalidParam(field, "{} is required!".format(field))
                no_exists.append(field)
        if include and len(no_exists) == len(include):
            raise InvalidParam(no_exists, f"{no_exists} are required!")

        for field in exclude:
            if not is_empty_value(retrieve_attr(self, field)):
                raise InvalidParam(field, "{} can't be received".format(field))


class RWModel(MongoModel):
    """
    MongoDB数据，具有读写权限，需要设定，创建和修改时间
    """
    create_time: Optional[datetime]
    update_time: Optional[datetime]

    is_deleted: Optional[bool]
    delete_time: Optional[datetime]

    @validator("update_time", pre=True, always=True)
    def val_update_time(cls, v):
        if not v:
            return date_now()

        return v